package org.budo.maven.plugin.assembly.mojo.processor;

import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Profile;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.project.MavenProject;
import org.budo.compress.Compressor;
import org.budo.compress.CompressorFactory;
import org.budo.support.lang.util.FileUtil;
import org.budo.support.lang.util.IoUtil;
import org.budo.support.lang.util.PropertiesUtil;
import org.budo.support.lang.util.StringUtil;
import org.budo.time.Time;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * @author lmw
 */
@Getter
@Setter
public abstract class AbstractBudoMavenAssemblyProcessor implements BudoMavenAssemblyProcessor {
    private static final String[] BIN_FILES = new String[] { "README.txt", "dump.sh", "start.sh", "stop.sh", "start.bat", "stop.bat", "update.bat", "app.exe" }; // TODO 应该可以改成扫描

    private static final String[] DOCKER_FILES = new String[] { "server.xml", "README.txt", "start.sh", "Dockerfile", "entrypoint-run.sh", "README.md", "run.sh", "stop.sh" }; // TODO 应该可以改成扫描

    private static final String JAVA_OPTS = "-Xrs -server " //
            + "-Xmx512m -Xms64m " //
            + "-XX:MaxPermSize=256m -XX:PermSize=16m " //
            + "-Xss256k -XX:SurvivorRatio=8 -XX:LargePageSizeInBytes=128m " //
            + "-Duser.timezone=GMT+8";

    protected static final String README_TEMPLATE = "README\r\n\r\n" //
            + "bin: the script to start & stop\r\n\r\n" //
            + "conf: configuration files\r\n\r\n" //
            + "lib: the jars\r\n\r\n" //
            + "webapp: html & web.xml\r\n\r\n";

    private MavenProject project;

    private MavenSession session;

    private String format;

    private Log log;

    public AbstractBudoMavenAssemblyProcessor(MavenProject project, MavenSession session, String format, Log log) {
        this.setProject(project);
        this.setSession(session);
        this.setFormat(format);
        this.setLog(log);
    }

    /**
     * 压缩
     */
    protected void compress() throws IOException {
        String target = this.getProject().getBuild().getDirectory();

        File dir = new File(target + File.separator + this.getProject().getArtifactId() + "-assembly");

        String[] fmts = this.getFormat().split(",");
        for (String fmt : fmts) {
            File zipFile = new File(target + File.separator + this.getProject().getArtifactId() + "-assembly" + "." + fmt.trim());

            Compressor compressor = CompressorFactory.getCompressor(zipFile.getName());
            compressor.compress(dir, zipFile);

            this.getLog().info("#304 done, compressor=" + compressor + ", canonicalPath=" + zipFile.getCanonicalPath() + ", this=" + this);
        }
    }

    protected void filterFile(File from, File to, Properties activeProfileProperties) {
        this.getLog().debug("#188 filterFile, from=" + from + ", to=" + to + ", properties=" + activeProfileProperties);

        // 被替换处理的行数
        int filteredLineCount = 0;

        List<String> lines = IoUtil.bufferedReadLineToStringList(IoUtil.fileInputStream(from));
        List<String> out = new ArrayList<String>();
        for (String line : lines) {
            String lineNew = null;
            int a = line.indexOf("${");
            if (a > 0) {
                int b = line.indexOf('}', a);
                if (b > 0) {
                    String left = line.substring(0, a);
                    String right = line.substring(b + 1);

                    String key = line.substring(a + 2, b);
                    String value = activeProfileProperties.getProperty(key);

                    if (null != value) {
                        lineNew = left + value + right;

                        this.getLog().info("#203 key=" + key + ", value=" + value + ", line=" + line + ", lineNew=" + lineNew);

                        filteredLineCount++;
                    }
                }
            }

            out.add(null == lineNew ? line : lineNew);
        }

        if (filteredLineCount > 0) {
            this.getLog().info("#217 from=" + from + ", to=" + FileUtil.getCanonicalPath(to) + ", properties=" + activeProfileProperties);
        }

        FileUtil.deleteIfExists(to);
        FileUtil.createNewFile(to);

        IoUtil.write(StringUtil.join(out, "\n").getBytes(), to);
    }

    // 如果 match 就不复制
    private boolean match(String filePath, String resourceDir, String exclude) {
        if (exclude.startsWith("*.")) {
            String substring = exclude.substring(1);
            if (filePath.endsWith(substring)) {
                return true;
            }
        }

        if (exclude.startsWith("**/*.")) {
            String substring = exclude.substring(4);
            if (filePath.endsWith(substring)) {
                return true;
            }
        }

        if (exclude.endsWith("/*")) {
            String substring = exclude.substring(0, exclude.length() - 2);
            String prefix = resourceDir + "/" + substring;
            if (filePath.startsWith(prefix)) {
                return true;
            }
        }

        // this.getLog().warn("#126 exclude=" + exclude);
        return false;
    }

    protected boolean matchAnyExcludes(File file, Resource resource) {
        for (String exclude : resource.getExcludes()) {
            String filePath = FileUtil.getCanonicalPath(file);
            String resourceDir = resource.getDirectory();
            if (this.match(filePath, resourceDir, exclude)) {
                return true; // 匹配到排除目录
            }
        }

        return false;
    }

    protected void copyDirAndFiltering(File from, File to, FileFilter filter, String filtering, Properties activeProfileProperties) {
        if (!from.exists() || !from.isDirectory()) {
            log.error("#178 from is not dir, from=" + from + ", to=" + to + ", filter=" + filter + ", filtering=" + filtering + ", activeProfileProperties=" + activeProfileProperties);
            return;
        }

        if (!to.exists()) {
            to.mkdirs();
        }

        File[] files = from.listFiles();

        for (File file : files) {
            if (null != filter && !filter.accept(file)) {
                continue;
            }

            if (file.isDirectory()) {
                this.copyDirAndFiltering(new File(from, file.getName()), new File(to, file.getName()), filter, filtering, activeProfileProperties);
                continue;
            }

            if (file.isFile()) {
                this.copyFileAndFiltering(new File(from, file.getName()), new File(to, file.getName()), filtering, activeProfileProperties);
                continue;
            }

            throw new RuntimeException("#79 file is not file or directory, file=" + file);
        }
    }

    /**
     * 写入一个说明文本
     */
    protected void writeReadme() throws IOException {
        String target = this.getProject().getBuild().getDirectory();
        String artifactId = this.getProject().getArtifactId();
        File assemblyDir = new File(target + File.separator + artifactId + "-assembly");

        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        this.getLog().info("#124 writeReadme, > " + assemblyDir.getCanonicalPath() + ", classLoader=" + classLoader);

        File to = new File(assemblyDir.getCanonicalPath() + File.separator + "README.txt");

        String mergedScript = this.mergeTemplate(README_TEMPLATE);
        IoUtil.write(mergedScript, new FileOutputStream(to));
    }

    private void copyFileAndFiltering(File from, File to, String filtering, Properties activeProfileProperties) {
        if (!to.exists()) {
            File parentFile = to.getParentFile();
            parentFile.mkdirs();
        }

        if ("true".equalsIgnoreCase(filtering)) {
            this.filterFile(from, to, activeProfileProperties);
        } else {
            IoUtil.write(from, to);
        }
    }

    protected String findInResource(String path) {
        List<Resource> resources = this.getProject().getResources();

        for (Resource resource : resources) {
            String srcPath = resource.getDirectory() //
                    + File.separator + path;

            File file = new File(srcPath);
            if (file.exists()) {
                this.getLog().debug("#301 this will skip, srcPath=" + srcPath);
                return FileUtil.getCanonicalPath(file);
            }
        }

        this.getLog().debug("#306 this will be in the jar, path=" + path);
        return null;
    }

    protected Properties getActiveProfileProperties() {
        Properties properties = new Properties();

        List<Profile> activeProfiles = this.getProject().getActiveProfiles();
        if (null == activeProfiles || activeProfiles.isEmpty()) {
            return properties;
        }

        for (Profile profile : activeProfiles) {
            if (null == profile) {
                continue;
            }

            Properties prop = profile.getProperties();
            if (null == prop) {
                continue;
            }

            properties.putAll(prop);
        }

        return properties;
    }

    /**
     * @see #copyConf(Properties)
     */
    protected void writeDocker() throws IOException {
        String target = this.getProject().getBuild().getDirectory();
        File dockerDir = new File(target + File.separator + this.getProject().getArtifactId() + "-assembly" + File.separator + "docker");

        // mkdirs
        dockerDir.mkdirs();

        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        this.getLog().info("#259 writeDocker, " + StringUtils.join(DOCKER_FILES, ", ") + " > " + dockerDir.getCanonicalPath() + ", classLoader=" + classLoader);

        URL assemblyDocker = classLoader.getResource("budo-maven-assembly-docker");

        String projectDockerPath = FileUtil.getCanonicalPath(this.getProject().getBasedir()) //
                + File.separator + "docker" + File.separator + "run";

        for (String dockerFile : DOCKER_FILES) {
            InputStream from = null;

            if (FileUtil.exists(projectDockerPath, dockerFile)) {
                from = IoUtil.fileInputStream(projectDockerPath + File.separator + dockerFile); // 项目里面自己带的
            } else {
                from = this.resourceInputStream("budo-maven-assembly-docker", dockerFile); // 公共的
            }

            if (null == from) {
                this.getLog().error("#266 dockerFile " + dockerFile + " not found, assemblyDocker=" + assemblyDocker);
                continue; // 没这文件
            }

            File to = new File(dockerDir.getCanonicalPath() + File.separator + dockerFile);

            // 脚本
            String scriptTemplate = IoUtil.toString(from);
            String mergedScript = this.mergeTemplate(scriptTemplate);
            IoUtil.write(mergedScript, new FileOutputStream(to));
        }
    }

    /**
     * 写入脚本
     */
    protected void writeBin() throws Throwable {
        String target = this.getProject().getBuild().getDirectory();
        File binDir = new File(target + File.separator + this.getProject().getArtifactId() + "-assembly" + File.separator + "bin");

        // mkdirs
        binDir.mkdirs();

        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        this.getLog().info("#124 writeBinFiles, " + StringUtils.join(BIN_FILES, ", ") + " > " + binDir.getCanonicalPath() + ", classLoader=" + classLoader);

        URL assemblyBin = classLoader.getResource("budo-maven-assembly-bin");

        for (String binFile : BIN_FILES) {
            InputStream from = this.resourceInputStream("budo-maven-assembly-bin", binFile);
            if (null == from) {
                this.getLog().error("#241 binFile " + binFile + " not found, assemblyBin=" + assemblyBin);
                continue;
            }

            File to = new File(binDir.getCanonicalPath() + File.separator + binFile);

            // exe
            if (binFile.endsWith(".exe")) {
                IoUtil.write(from, to);
                continue;
            }

            // 脚本
            String scriptTemplate = IoUtil.toString(from);
            String mergedScript = this.mergeTemplate(scriptTemplate);
            IoUtil.write(mergedScript, new FileOutputStream(to));
        }
    }

    /**
     * 默认值
     */
    private Properties defaultProperties() {
        Properties properties = new Properties();
        properties.setProperty("AUTOMAN_JAVA_OPTS", JAVA_OPTS);
        properties.setProperty("AUTOMAN_JAVA_ARGS", ""); // TODO 这个没搞?
        properties.setProperty("AUTOMAN_HOST", "127.0.0.1");
        properties.setProperty("AUTOMAN_PORT", "-1");
        properties.setProperty("AUTOMAN_APPLICATION_NAME", this.getProject().getArtifactId());
        properties.setProperty("AUTOMAN_PROTOCOL", "not-dubbo");
        properties.setProperty("AUTOMAN_LAUNCHER", "budo.Launcher");

        String now = Time.now().toString("yyyy-MM-dd HH:mm:ss.SSS");
        properties.setProperty("BuildAt", now);
        return properties;
    }

    /**
     * 项目自定义配置，从 automan.conf 中读取
     */
    private Properties automanProperties() {
        String automanPath = FileUtil.getCanonicalPath(this.getProject().getBasedir()) //
                + File.separator + "src" + File.separator + "main" + File.separator + "resources" //
                + File.separator + "automan.conf";

        File automanFile = new File(automanPath);
        if (automanFile.exists() && automanFile.isFile() && automanFile.canRead()) {
            FileInputStream fileInputStream = IoUtil.fileInputStream(automanPath);
            return PropertiesUtil.load(fileInputStream);
        }

        return new Properties();
    }

    /**
     * 替换脚本模板中的变量
     */
    protected String mergeTemplate(String scriptTemplate) {
        Properties properties = this.defaultProperties();

        Properties automanProperties = this.automanProperties();
        for (Entry<Object, Object> entry : automanProperties.entrySet()) {
            String key = (String) entry.getKey();
            String value = (String) entry.getValue();

            String k = key.replace("automan.", "");
            properties.put(k, value); // 去前缀，合并
        }

        // 替换变量
        for (Entry<Object, Object> entry : properties.entrySet()) {
            String key = (String) entry.getKey();
            String value = (String) entry.getValue();

            scriptTemplate = scriptTemplate.replace("#{" + key + "}", value);
        }

        return scriptTemplate;
    }

    protected InputStream resourceInputStream(String folder, String file) throws IOException {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        URL assemblyFolder = classLoader.getResource(folder);

        URL from = classLoader.getResource(folder + File.separator + file);
        if (null != from) {
            return from.openStream();
        }

        JarURLConnection jarURLConnection = (JarURLConnection) assemblyFolder.openConnection();
        if (null == jarURLConnection) {
            this.getLog().error("#249 jarURLConnection is null, assemblyFolder=" + assemblyFolder);
            return null;
        }

        JarFile jarFile = jarURLConnection.getJarFile();
        if (null == jarFile) {
            this.getLog().error("#249 jarFile is null, jarURLConnection=" + jarURLConnection);
            return null;
        }

        Enumeration<JarEntry> enumeration = jarFile.entries();
        while (enumeration.hasMoreElements()) {
            JarEntry jarEntry = (JarEntry) enumeration.nextElement();
            if (jarEntry.getName().endsWith(file)) {
                return jarFile.getInputStream(jarEntry);
            }
        }

        this.getLog().warn("#279 file " + file + " not found, assemblyFolder=" + assemblyFolder);
        return null;
    }
}
